﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Microsoft.Xrm.Client.Data.Services;
using Microsoft.Xrm.Client;

namespace Kocic.RepositoryManagement.Crm4
{
    /// <summary>
    /// Defines methods and operations for managers to perform create, read, update, and delete operations on a CRM entity object, in a CRM data context, through advanced developer extensions for CRM 4.0.
    /// </summary>
    /// <typeparam name="TCrmEntityObject">The type of the CRM entity object that the CRM data manager operations will be implemented for.</typeparam>
    /// <typeparam name="TContext">The type of the CRM data context that will be used by the CRM data managers.</typeparam>
    public abstract class XrmRepositoryManager<TCrmEntityObject, TContext> : DataManager<TCrmEntityObject, TContext>, IRetrievable<TCrmEntityObject>
        where TCrmEntityObject : CrmEntity
        where TContext : CrmDataContext, new()
    {
        /// <summary>
        /// Creates an instance of the CrmDataManager class.
        /// </summary>
        /// <param name="context">An instance of the CRM data context that will be used for operations.</param>
        /// <exception cref="ArgumentNullException"></exception>
        protected XrmRepositoryManager(TContext context)
            : base(context)
        {
            if (XrmCacheManager.CachingInstance != null)
            {
                XrmCacheManager.ClearCache<TCrmEntityObject>();
            }
        }

        /// <summary>
        /// Creates an instance of the XrmDataManager class. Invoking this constructor will pass the static instance reference to the object context referenced an instance of the XrmContextScope class.
        /// </summary>
        protected XrmRepositoryManager()
            : this(XrmExecutionContext<TContext>.CurrentDataSource)
        {
            if (XrmCacheManager.CachingInstance != null)
            {
                XrmCacheManager.ClearCache<TCrmEntityObject>();
            }
        }

        /// <summary>
        /// Disposes unmanaged resources.
        /// </summary>
        /// <param name="disposing">Value indicating if to invoke the method from the IDisposable.Dispose implementation or from the finalizer.</param>
        protected override void Dispose(Boolean disposing)
        {
            if (disposing)
            {
                if (this.IsDisposed)
                {
                    return;
                }

                if (XrmExecutionContext<TContext>.CurrentDataSource != null)
                {
                    this.DisposeDataSource = false;
                }

                if (this.DisposeDataSource)
                {
                    this.DataSource = null;
                }

                base.Dispose(disposing);
            }
        }

        /// <summary>
        /// Saves data object to data source. Creates new data or updates existing data.
        /// </summary>
        /// <param name="crmEntityObject">The CRM data object to save to CRM.</param>
        /// <param name="method">The method of saving a CRM data object to the data source.</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentException"></exception>
        public override void Save(TCrmEntityObject crmEntityObject, SaveMethod method)
        {
            if (method == SaveMethod.None)
            {
                throw new ArgumentException("Save method must be either Create or Update.", "method");
            }

            base.Save(crmEntityObject, method);
        }


        /// <summary>
        /// Saves data object to data source. Creates new data or updates existing data. Creates new or updates existing relationships between entity objects.
        /// </summary>
        /// <param name="crmEntityObject">The CRM data object to save to CRM.</param>
        /// <param name="method">The method of saving a CRM data object to the data source.</param>
        /// <param name="relationships">Actions associated with relationships</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentException"></exception>
        public virtual void Save(TCrmEntityObject crmEntityObject, SaveMethod method, XrmRelationshipInfo[] relationships)
        {
            if (method == SaveMethod.None)
            {
                throw new ArgumentException("Save method must be either Create or Update.", "method");
            }

            if (relationships == null)
            {
                throw new ArgumentException("Relationships cannot be null.", "relationships");
            }

            base.Save(crmEntityObject, method);

            foreach (XrmRelationshipInfo info in relationships)
            {
                if (method == SaveMethod.Create)
                {
                    XrmRepositoryManager<TCrmEntityObject, TContext>.PerformRelationshipOperation(info, this.DataSource, (inInfo, inContext) => inContext.AddLink(inInfo.Primary, inInfo.Name, info.Related));
                }
                else if (method == SaveMethod.Update)
                {
                    XrmRepositoryManager<TCrmEntityObject, TContext>.PerformRelationshipOperation(info, this.DataSource, (inInfo, inContext) => inContext.SetLink(inInfo.Related, inInfo.Name, inInfo.Primary));
                }
            }
        }

        /// <summary>
        /// Saves data object to data source. Creates new data or updates existing data. Creates new or updates existing relationships between entity objects.
        /// </summary>
        /// <param name="crmEntityObject">The CRM data object to save to CRM.</param>
        /// <param name="method">The method of saving a CRM data object to the data source.</param>
        /// <param name="relationshipAssociationCallback">Callback method</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentException"></exception>
        public void Save(TCrmEntityObject crmEntityObject, SaveMethod method, Action relationshipAssociationCallback)
        {
            if (method == SaveMethod.None)
            {
                throw new ArgumentException("Save method must be either Create or Update.", "method");
            }

            if (relationshipAssociationCallback == null)
            {
                throw new ArgumentException("Relationship callback cannot be null.", "relationshipAssociationCallback");
            }

            base.Save(crmEntityObject, method);

            relationshipAssociationCallback();
        }


        /// <summary>
        /// Saves changes to the CRM data context and commits them to CRM.
        /// </summary>
        /// <param name="crmEntityObject">The entity object to commit to the CRM.</param>
        protected override void Commit(TCrmEntityObject crmEntityObject = null)
        {
            this.DataSource.SaveChanges();
        }

        /// <summary>
        /// Performs operations to add the CRM entity object to the object context.
        /// </summary>
        /// <param name="crmEntityObject">The CRM entity object to add.</param>
        protected override void DataSourceCreateOperations(TCrmEntityObject crmEntityObject)
        {
            this.DataSource.AddObject((crmEntityObject as ICrmEntity).LogicalName, crmEntityObject);
        }

        /// <summary>
        /// Performs operations to update the CRM entity object in the object context.
        /// </summary>
        /// <param name="crmEntityObject">The CRM entity object to update.</param>
        protected override void DataSourceUpdateOperations(TCrmEntityObject crmEntityObject)
        {
            this.DataSource.UpdateObject(crmEntityObject);
        }

        /// <summary>
        /// Performs operations to remove the entity object from the object context.
        /// </summary>
        /// <param name="crmEntityObject">The entity object to remove.</param>
        protected override void DataSourceDeleteOperations(TCrmEntityObject crmEntityObject)
        {
            this.DataSource.DeleteObject(crmEntityObject);
        }

        /// <summary>
        /// Checks to see if a XrmContextDeferrer instance was instantiated and a reference exists.
        /// </summary>
        /// <returns>Value indicating if the XrmContextDeferrer was instantiated and a reference exists.</returns>
        protected override bool IsDeferralInstanceInstantiated()
        {
            return (XrmContextDeferrer<TContext>.DeferralOperation != null);
        }

        /// <summary>
        /// Executes any deferral operations for the provided entity object.
        /// </summary>
        /// <param name="crmEntityObject">The entity object to defer operations on.</param>
        protected override void DeferrerOperations(TCrmEntityObject crmEntityObject)
        {
            if (this.IsDeferralInstanceInstantiated())
            {
                XrmContextDeferrer<TContext>.DeferredObjects.Add(crmEntityObject);
            }
        }

        /// <summary>
        /// Performs any additional operations with the CRM entity object before it is created and added to the context.
        /// </summary>
        /// <param name="crmEntityObject">The CRM entity object used in performing additional operations to prior to adding and saving the changes to the context.</param>
        protected override TCrmEntityObject PreCreate(TCrmEntityObject crmEntityObject)
        {
            return null;
        }

        /// <summary>
        /// Performs any additional operations with the CRM entity object before it is updated and its changes saved to the context.
        /// </summary>
        /// <param name="crmEntityObject">The CRM entity object used in performing additional operations to prior to saving its changes to the context.</param>
        protected override TCrmEntityObject PreUpdate(TCrmEntityObject crmEntityObject)
        {
            return null;
        }

        /// <summary>
        /// Performs any additional operations with the CRM entity object before it is deleted and removed from the context.
        /// </summary>
        /// <param name="crmEntityObject">The CRM entity object used in performing additional operations to prior to deleting it from the context and saving the changes.</param>
        protected override TCrmEntityObject PreDelete(TCrmEntityObject crmEntityObject)
        {
            return null;
        }

        #region IRetrievable<TCrmEntityObject> Members

        /*These methods have been marked as abstract since the manager must be required to provide the basic read functionality */

        /// <summary>
        /// Defines how to return a single instance of the CRM entity object based on the specified search value.
        /// </summary>
        /// <param name="searchValue">An object used to store particular search criteria.</param>
        /// <returns>The entity object that is a result of a search.</returns>
        public abstract TCrmEntityObject RetrieveSingle(Object searchValue);

        /// <summary>
        /// Defines how to retrieve multiple instances of the CRM entity object.
        /// </summary>
        /// <returns>An enumerator of a collection of type TCrmEntityObject.</returns>
        public abstract IEnumerable<TCrmEntityObject> RetrieveMultiple();

        /// <summary>
        /// Defines how to retrieve multiple instances of the CRM entity object based on the specified search value.
        /// </summary>
        /// <param name="searchValue">An object used to store particular search criteria.</param>
        /// <returns>An enumerator of a collection of type TCrmEntityObject that is the result of a search.</returns>
        public abstract IEnumerable<TCrmEntityObject> RetrieveMultiple(Object searchValue);

        #endregion

        /// <summary>
        /// Saves a CRM entity object to the context and CRM. Either creates or updates the CRM entity object based on the provided save method. If an instance of the
        /// XrmContextDeferral instance exists, then the CRM entity object is simply added to the context and no changes are saved.
        /// </summary>
        /// <param name="crmEntityObject">The CRM entity object to save to the context and CRM.</param>
        /// <param name="method">The method of saving a CRM entity object.</param>
        /// <param name="context">A instance of the CRM data context to which the CRM entity object will be saved to. If this instance is null, then a data context instance referenced
        /// by a XrmExecutionContext instance will be used. If this instance is not null and an instance of the XrmExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        public static void SaveCrmEntityObject(TCrmEntityObject crmEntityObject, SaveMethod method, TContext context = null)
        {
            if (method == SaveMethod.None)
            {
                throw new ArgumentException("Save method must be either Create or Update.", "method");
            }

            XrmRepositoryManager<TCrmEntityObject, TContext>.ContextOperations(crmEntityObject, context, (inCrmEntityObject, inContext) =>
            {
                if (method == SaveMethod.Create)
                {
                    inContext.AddObject((inCrmEntityObject as ICrmEntity).LogicalName, crmEntityObject);
                }
                else
                {
                    inContext.UpdateObject(inCrmEntityObject);
                }
            });
        }

        /// <summary>
        /// Saves a CRM entity object to the context and CRM. Either creates or updates the CRM entity object based on the provided save method. If an instance of the
        /// XrmContextDeferral instance exists, then the CRM entity object is simply added to the context and no changes are saved.
        /// </summary>
        /// <param name="crmEntityObject">The CRM entity object to save to the context and CRM.</param>
        /// <param name="method">The method of saving a CRM entity object.</param>
        /// <param name="relationships">Actions associated with relationships</param>
        /// <param name="context">A instance of the CRM data context to which the CRM entity object will be saved to. If this instance is null, then a data context instance referenced
        /// by a XrmExecutionContext instance will be used. If this instance is not null and an instance of the XrmExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        public static void SaveCrmEntityObject(TCrmEntityObject crmEntityObject, SaveMethod method, XrmRelationshipInfo[] relationships, TContext context = null)
        {
            if (method == SaveMethod.None)
            {
                throw new ArgumentException("Save method must be either Create or Update.", "method");
            }

            if (relationships == null)
            {
                throw new ArgumentException("Relationships cannot be null.", "relationships");
            }

            XrmRepositoryManager<TCrmEntityObject, TContext>.ContextOperations(crmEntityObject, context, (inCrmEntityObject, inContext) =>
            {
                if (method == SaveMethod.Create)
                {
                    inContext.AddObject((inCrmEntityObject as ICrmEntity).LogicalName, crmEntityObject);
                }
                else
                {
                    inContext.UpdateObject(inCrmEntityObject);
                }

                foreach (XrmRelationshipInfo info in relationships)
                {
                    if (method == SaveMethod.Create)
                    {
                        XrmRepositoryManager<TCrmEntityObject, TContext>.PerformRelationshipOperation(info, context, (inInfo, inContext2) => inContext2.AddLink(inInfo.Primary, inInfo.Name, info.Related));
                    }
                    else if (method == SaveMethod.Update)
                    {
                        XrmRepositoryManager<TCrmEntityObject, TContext>.PerformRelationshipOperation(info, context, (inInfo, inContext2) => inContext2.SetLink(inInfo.Related, inInfo.Name, inInfo.Primary));
                    }
                }
            });
        }

        /// <summary>
        /// Saves a CRM entity object to the context and CRM. Either creates or updates the CRM entity object based on the provided save method. If an instance of the
        /// XrmContextDeferral instance exists, then the CRM entity object is simply added to the context and no changes are saved.
        /// </summary>
        /// <param name="crmEntityObject">The CRM entity object to save to the context and CRM.</param>
        /// <param name="method">The method of saving a CRM entity object.</param>
        /// <param name="relationshipAssociationCallback">Callback method</param>
        /// <param name="context">A instance of the CRM data context to which the CRM entity object will be saved to. If this instance is null, then a data context instance referenced
        /// by a XrmExecutionContext instance will be used. If this instance is not null and an instance of the XrmExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        public static void SaveCrmEntityObject(TCrmEntityObject crmEntityObject, SaveMethod method, Action relationshipAssociationCallback, TContext context = null)
        {
            if (method == SaveMethod.None)
            {
                throw new ArgumentException("Save method must be either Create or Update.", "method");
            }

            if (relationshipAssociationCallback == null)
            {
                throw new ArgumentException("Relationship asssociation callback cannto be null.", "relationshipAssociationCallback");
            }

            XrmRepositoryManager<TCrmEntityObject, TContext>.ContextOperations(crmEntityObject, context, (inCrmEntityObject, inContext) =>
            {
                if (method == SaveMethod.Create)
                {
                    inContext.AddObject((inCrmEntityObject as ICrmEntity).LogicalName, crmEntityObject);
                }
                else
                {
                    inContext.UpdateObject(inCrmEntityObject);
                }

                relationshipAssociationCallback();
            });
        }

        /// <summary>
        /// Retrieves and returns IQueryable&lt;TCrmEntityObject> object.
        /// </summary>
        /// <param name="whereExpression">Where expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a XrmExecutionContext instance will be used. If this instance is not null and an instance of the XrmExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <returns>IQueryable&lt;TEntityObject> object.</returns>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static IQueryable<TCrmEntityObject> Retrieve(Expression<Func<TCrmEntityObject, Boolean>> whereExpression, TContext context = null)
        {
            if (XrmCacheManager.CachingInstance != null)
            {
                XrmCacheManager.ClearCache<TCrmEntityObject>();
            }

            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (XrmExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = XrmExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && XrmExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the XrmExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            return currentContext.GetEntities<TCrmEntityObject>().Where(whereExpression);
        }

        /// <summary>
        /// Retrieves and returns IQueryable&lt;TCrmEntityObject> object.
        /// </summary>
        /// <param name="retrieveExpression">Retrieve expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a XrmExecutionContext instance will be used. If this instance is not null and an instance of the XrmExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <returns>IQueryable&lt;TEntityObject> object.</returns>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static IQueryable<TCrmEntityObject> Retrieve(Expression<Func<TCrmEntityObject, TCrmEntityObject>> retrieveExpression, TContext context = null)
        {
            if (XrmCacheManager.CachingInstance != null)
            {
                XrmCacheManager.ClearCache<TCrmEntityObject>();
            }

            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (XrmExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = XrmExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && XrmExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the XrmExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            return currentContext.GetEntities<TCrmEntityObject>().Select(retrieveExpression);
        }

        /// <summary>
        /// Deletes entity objects based on the provided where expression.
        /// </summary>
        /// <param name="whereExpression">Where expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a XrmExecutionContext instance will be used. If this instance is not null and an instance of the XrmExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static void DeleteCrmEntityObjects(Expression<Func<TCrmEntityObject, Boolean>> whereExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (XrmExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = XrmExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && XrmExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the XrmExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            var objects = currentContext.GetEntities<TCrmEntityObject>().Where(whereExpression).ToList();

            foreach (var o in objects)
            {
                XrmRepositoryManager<TCrmEntityObject, TContext>.DeleteCrmEntityObject(o);
            }
        }

        /// <summary>
        /// Deletes entity objects based on the provided select expression.
        /// </summary>
        /// <param name="retrieveExpression">Retrieve expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a XrmExecutionContext instance will be used. If this instance is not null and an instance of the XrmExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <returns>IQueryable&lt;TEntityObject> object.</returns>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static void DeleteCrmEntityObjects(Expression<Func<TCrmEntityObject, TCrmEntityObject>> retrieveExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (XrmExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = XrmExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && XrmExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the XrmExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            var objects = currentContext.GetEntities<TCrmEntityObject>().Select(retrieveExpression).ToList();

            foreach (var o in objects)
            {
                XrmRepositoryManager<TCrmEntityObject, TContext>.DeleteCrmEntityObject(o);
            }
        }

        /// <summary>
        /// Removes a data object from the CRM data context and CRM. If an instance of the
        /// XrmContextDeferral instance exists, then the CRM entity object is simply removed and no changes are saved.
        /// </summary>
        /// <param name="crmEntityObject">The entity object to delete and remove from the data context.</param>
        /// <param name="context">A instance of the CRM data context to which the entity object will be removed from. If this instance is null, then a data context instance referenced
        /// by a XrmExecutionContext instance will be used. If this instance is not null and an instance of the XrmExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        public static void DeleteCrmEntityObject(TCrmEntityObject crmEntityObject, TContext context = null)
        {
            XrmRepositoryManager<TCrmEntityObject, TContext>.ContextOperations(crmEntityObject, context, (inEntityObject, inContext) => inContext.DeleteObject(inEntityObject));
        }

        /// <summary>
        /// Helper method which has the same logic when trying to add/remove entity objects in/from a context. Because the same logic is used, a delegate to the context's
        /// AddObject/DeleteObject methods is used.
        /// </summary>
        /// <param name="crmEntityObject">The entity object for which the appropriate action will take place.</param>
        /// <param name="context">The object context for which the action upon the entity object will take place.</param>
        /// <param name="contextAction">A delegate to a method that will execute the appropriate context operations.</param>
        private static void ContextOperations(TCrmEntityObject crmEntityObject, TContext context, Action<TCrmEntityObject, TContext> contextAction)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (XrmExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = XrmExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && XrmExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the EntityExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not bee instantiated.");
            }

            bool toDefer = XrmContextDeferrer<TContext>.DeferralOperation != null ? true : false;

            contextAction(crmEntityObject, currentContext);

            if (!toDefer)
            {
                currentContext.SaveChanges();
            }
            else
            {
                XrmContextDeferrer<TContext>.DeferredObjects.Add(crmEntityObject);
            }
        }

        /// <summary>
        /// Creates a relationship between two CRM entity objects.
        /// </summary>
        /// <param name="info">An instance of RelationshipInfo containing specific relationship information.</param>
        /// <param name="context">An instance of TContext that will be used in saving relationship information. If no value is provided, this method will reference an instance of the CRM data context
        /// in an instance of the XrmContextScope class.</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="InvalidOperationException"></exception>
        public static void CreateRelationship(XrmRelationshipInfo info, TContext context = null)
        {
            XrmRepositoryManager<TCrmEntityObject, TContext>.PerformRelationshipOperation(info, context, (inInfo, inContext) => inContext.AddLink(inInfo.Primary, inInfo.Name, info.Related));
        }

        /// <summary>
        /// Deletes a relationship between two CRM entity objects.
        /// </summary>
        /// <param name="info">An instance of RelationshipInfo containing specific relationship information.</param>
        /// <param name="context">An instance of TContext that will be used in saving relationship information. If no value is provided, this method will reference an instance of the CRM data context
        /// in an instance of the XrmContextScope class.</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="InvalidOperationException"></exception>
        public static void DeleteRelationship(XrmRelationshipInfo info, TContext context = null)
        {
            XrmRepositoryManager<TCrmEntityObject, TContext>.PerformRelationshipOperation(info, context, (inInfo, inContext) => inContext.SetLink(inInfo.Related, inInfo.Name, null));
        }

        /// <summary>
        /// Updates a relationship between two CRM entity objects.
        /// </summary>
        /// <param name="info">An instance of RelationshipInfo containing specific relationship information.</param>
        /// <param name="context">An instance of TContext that will be used in saving relationship information. If no value is provided, this method will reference an instance of the CRM data context
        /// in an instance of the XrmContextScope class.</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="InvalidOperationException"></exception>
        public static void UpdateRelationship(XrmRelationshipInfo info, TContext context = null)
        {
            XrmRepositoryManager<TCrmEntityObject, TContext>.PerformRelationshipOperation(info, context, (inInfo, inContext) => inContext.SetLink(inInfo.Related, inInfo.Name, inInfo.Primary));
        }

        /// <summary>
        /// Helper method in performing relationship operations.
        /// </summary>
        /// <param name="info">An instance of RelationshipInfo containing specific relationship information.</param>
        /// <param name="context">An instance of TContext that will be used in saving relationship information. If no value is provided, this method will reference an instance of the CRM data context
        /// in an instance of the XrmContextScope class.</param>
        /// <param name="contextAction">A delegate to a method that will execute the appropriate relationship operations.</param>
        private static void PerformRelationshipOperation(XrmRelationshipInfo info, TContext context, Action<XrmRelationshipInfo, TContext> contextAction)
        {
            TContext currentContext;

            if (info == null)
            {
                throw new ArgumentNullException("info", "An instance of RelationshipInfo must be provided.");
            }

            if (context == null)
            {
                if (XrmExecutionContext<TContext>.CurrentDataSource == null)
                {
                    throw new InvalidOperationException("The CRM object context must be initialized through an instance of the XrmContextScope class.");
                }

                currentContext = XrmExecutionContext<TContext>.CurrentDataSource;
            }
            else
            {
                currentContext = context;
            }

            contextAction(info, currentContext);
            currentContext.SaveChanges();
        }
    }
}